#include "stdafx.h"
#include "serialise/database.h"
#include "serialise/jsonserialise.h"
#include <fstream>

namespace Json {
namespace Schema
{

std::string GetTypeName(const FieldInfo& typeInfo)
{
	switch (typeInfo.basicType)
	{
		case Type::Null: return "Null";
		case Type::Boolean: return "Boolean";
		case Type::Int: return "Int";
		case Type::Float: return "Float";
		case Type::String: return "String";
		case Type::Object: return typeInfo.typeName;
		case Type::Enum: return typeInfo.typeName;
		case Type::Reference: return typeInfo.typeName;
		default: Assert(0); return "";
	}
}

bool HasMember(const Object& object, const char* name)
{
	for (auto& field : object.fields)
		if (field.first == name)
			return true;

	return false;
}

Type GetBasicType(const char* name)
{
	if (strcmp("Boolean", name) == 0)
		return Type::Boolean;
	else if (strcmp("Int", name) == 0)
		return Type::Int;
	else if (strcmp("Float", name) == 0)
		return Type::Float;
	else if (strcmp("String", name) == 0)
		return Type::String;

	return Type::Null;
}

void Object::RenameField(const std::string& oldName, const std::string& newName, int revision)
{
	Rename rename;
	rename.newName = newName;
	rename.oldName = oldName;
	rename.revision = revision;
	m_rename.push_back(rename);

	auto it = fields.find(oldName);
	FieldInfo info = it->second;
	fields.erase(it);
	fields.emplace(newName, info);
}

void Enum::RenameValue(const std::string& oldName, const std::string& newName, int revision)
{
	Rename rename;
	rename.newName = newName;
	rename.oldName = oldName;
	rename.revision = revision;
	m_rename.push_back(rename);

	values.erase(oldName);
	values.insert(newName);
}

}

std::string FolderName(const std::string name)
{
	std::string newString(name);

	newString.erase(std::remove(newString.begin(), newString.end(), ' '), newString.end());

	if (newString.size() > 0)
		newString[0] = toupper(newString[0]);

	return newString;
}

std::string SchemaName(const std::string name)
{
	std::string newString(name);

	newString.erase(std::remove(newString.begin(), newString.end(), ' '), newString.end());

	if (newString.size() > 0)
		newString[0] = toupper(newString[0]);

	return newString;
}

std::string RecordName(const std::string name)
{
	std::string newString(name);

	newString.erase(std::remove(newString.begin(), newString.end(), ' '), newString.end());

	if (newString.size() > 0)
		newString[0] = tolower(newString[0]);

	return newString;
}

Folder* Folder::InsertFolder(const std::string& name)
{
	std::string folderName = FolderName(name);

	if (!IsValidName(folderName))
		return 0;

	return &m_folders.emplace(folderName, Folder()).first->second;
}

Record* Folder::InsertRecord(const std::string& name)
{
	std::string recordName = RecordName(name);

	if (!IsValidName(recordName))
		return 0;

	return &m_records.emplace(recordName, Record()).first->second;
}

bool Folder::Rename(const std::string& oldName, const std::string& newName)
{
	if (!IsValidName(newName))
		return false;
	
	{
		auto it = m_folders.find(FolderName(oldName));

		if (it != m_folders.end())
		{
			auto temp = it->second;
			m_folders.erase(it);
			m_folders[FolderName(newName)] = temp;
			return true;
		}
	}

	{
		auto it = m_records.find(RecordName(oldName));

		if (it != m_records.end())
		{
			auto temp = it->second;
			m_records.erase(it);
			m_records[RecordName(newName)] = temp;
			return true;
		}
	}

	return false;
}

void Folder::Delete(const std::string& name)
{
	m_folders.erase(name);
	m_records.erase(name);
}

Folder* Folder::GetFolder(const std::string& name)
{
	auto it = m_folders.find(name);

	if (it != m_folders.end())
		return &it->second;

	return 0;
}

Record* Folder::GetRecord(const std::string& name)
{
	std::string recordName = RecordName(name);

	auto it = m_records.find(recordName);

	if (it != m_records.end())
		return &it->second;

	return 0;
}

Folder::FolderRange Folder::GetFolderRange()
{
	return FolderRange(m_folders.begin(), m_folders.end());
}

Folder::RecordRange Folder::GetRecordRange()
{
	return RecordRange(m_records.begin(), m_records.end());
}

bool Folder::IsValidName(const std::string newName) const
{
	if (newName == "")
		return false;

	if (m_folders.find(FolderName(newName)) != m_folders.end())
		return false;

	if (m_records.find(RecordName(newName)) != m_records.end())
		return false;

	return true;
}

Record* Folder::FindRecord(const std::string& location)
{
	std::string::size_type pos = location.find("/");

	if (pos != std::string::npos)
	{
		std::string folder = FolderName(location.substr(0, pos));
		auto it = m_folders.find(folder);

		if (it == m_folders.end())
			return 0;

		return it->second.FindRecord(location.substr(pos + 1, location.size() - pos));
	}
	else
		return GetRecord(location);
}

Folder* Folder::FindFolder(const std::string& location)
{
	std::string folder = FolderName(location);

	std::string::size_type pos = location.find("/");

	if (pos != std::string::npos)
		folder = location.substr(0, pos);

	auto it = m_folders.find(folder);

	if (it == m_folders.end())
		return 0;

	return &it->second;
}

//////////////////////////////////////////////////////////////////////////////

RecordIterator::RecordIterator(Folder::FolderRange::Iterator& root)
{
	Folder::FolderRange::Iterator next = root;
	m_folderStack.emplace_back(Folder::FolderRange(root, ++next), root);
	BeginFolder();
}

bool RecordIterator::operator != (const RecordIterator& other) const
{
	if (m_folderStack.empty() && other.m_folderStack.empty())
		return false;

	return m_folderStack != other.m_folderStack || m_recordIterator != other.m_recordIterator;
}

void RecordIterator::HandleEndRecords()
{
	Assert(!m_folderStack.empty());

	if (m_recordIterator == m_folderStack.back().second->second.GetRecordRange().end())
	{
		m_folderStack.back().second++;

		if (m_folderStack.back().second != m_folderStack.back().first.end())
		{
			BeginFolder();
		}
		else
		{
			m_folderStack.pop_back();

			if (!m_folderStack.empty())
				BeginRecords();
		}
	}
}

void RecordIterator::BeginFolder()
{
	Folder::FolderRange folderRange = m_folderStack.back().second->second.GetFolderRange();

	while (folderRange.begin() != folderRange.end())
	{
		m_folderStack.emplace_back(folderRange, folderRange.begin());
		folderRange = folderRange.begin()->second.GetFolderRange();
	}

	BeginRecords();
}

void RecordIterator::BeginRecords()
{
	m_recordIterator = m_folderStack.back().second->second.GetRecordRange().begin();

	HandleEndRecords();
}

////////////////////////////////////////////////////////////////////////////////

Database::Database()
{
	m_folders.emplace("Root", Folder());

	m_revision = 0;
}

void Database::SetName(const std::string& name)
{
	m_name = name;
}

const std::string& Database::GetName() const 
{
	return m_name;
}

Schema::Object* Database::InsertSchema(const std::string& name)
{
	Assert(name != "");
	return &m_schemas[SchemaName(name)];
}

Schema::Object* Database::GetSchema(const std::string& name)
{
	auto it = m_schemas.find(SchemaName(name));
	if (it != m_schemas.end())
		return &it->second;

	return 0;
}

const Schema::Object* Database::GetSchema(const std::string& name) const
{
	return const_cast<Database*>(this)->GetSchema(name);
}

void Database::EraseSchema(const std::string& name)
{
	m_schemas.erase(SchemaName(name));
}

void Database::RenameSchema(const std::string& oldName, const std::string& newName)
{
	auto it = m_schemas.find(SchemaName(newName));
	if (it != m_schemas.end())
		return;

	it = m_schemas.find(SchemaName(oldName));
	if (it != m_schemas.end())
	{
		for (Record& record : GetRecordRange())
			if (record.schema == it->first)
				record.schema = SchemaName(newName);

		Schema::Object temp = it->second;
		m_schemas.erase(it);
		m_schemas[SchemaName(newName)] = temp;
	}
}

Database::SchemaRange Database::GetSchemaRange() const
{
	return SchemaRange(m_schemas.begin(), m_schemas.end());
}

Schema::Enum* Database::InsertEnum(const std::string& name)
{
	Assert(name != "");
	return &m_enums[SchemaName(name)];
}

Schema::Enum* Database::GetEnum(const std::string& name)
{
	auto it = m_enums.find(SchemaName(name));
	if (it != m_enums.end())
		return &it->second;

	return 0;
}

void Database::EraseEnum(const std::string& name)
{
	m_enums.erase(SchemaName(name));
}

void Database::RenameEnum(const std::string& oldName, const std::string& newName)
{
	auto it = m_enums.find(SchemaName(newName));
	if (it != m_enums.end())
		return;

	it = m_enums.find(SchemaName(oldName));
	if (it != m_enums.end())
	{
		Schema::Enum temp = it->second;
		m_enums.erase(it);
		m_enums[SchemaName(newName)] = temp;
	}

	// to do: rename enums in schemas
}

Database::EnumRange Database::GetEnumRange() const
{
	return EnumRange(m_enums.begin(), m_enums.end());
}

Database::RecordRange Database::GetRecordRange() 
{
	return Ut::MakeRange(RecordIterator(m_folders.begin()), RecordIterator());
}

Folder& Database::GetRoot()
{
	return m_folders.begin()->second;
}

void Database::Schematise(Json::JData* jData, int revision, Schema::Type basicType, const std::string typeName)
{
	if (basicType == Json::Schema::Type::Null)
	{
		*jData->GetString() = "Null";
	}
	else if (basicType == Json::Schema::Type::Boolean)
	{
		bool temp = false;
		ReadValue(*jData->GetString(), temp);
		*jData->GetString() = (temp ? "true" : "false");
	}
	else if (basicType == Json::Schema::Type::Int)
	{
		int temp = 0;
		ReadValue(*jData->GetString(), temp);
		std::stringstream ss;
		ss << temp;
		*jData->GetString() = ss.str();
	}
	else if (basicType == Json::Schema::Type::Float)
	{
		float temp = 0.f;
		ReadValue(*jData->GetString(), temp);
		std::stringstream ss;
		ss << temp;
		*jData->GetString() = ss.str();
	}
	else if (basicType == Json::Schema::Type::String || basicType == Json::Schema::Type::Reference)
	{
		std::string s = *jData->GetString();
		if (s.empty()) s = "\"\"";
		if (s[0] != '\"') s = "\"" + s + "\"";
		*jData->GetString() = s;
	}
	else if (basicType == Json::Schema::Type::Enum)
	{
		std::string s = *jData->GetString();
		if (s.empty()) s = "\"Null\"";
		if (s[0] != '\"') s = "\"" + s + "\"";
		*jData->GetString() = s;

		Schema::Enum* pEnum = GetEnum(typeName);

		if (pEnum)
		{
			for (auto& rename : pEnum->m_rename)
			{
				if (revision < rename.revision && *jData->GetString() == "\"" + rename.oldName + "\"")
				{
					*jData->GetString() = "\"" + rename.newName + "\"";
				}
			}
		}
	}
}

void Database::Schematise(Record& record, const Schema::Object& schema)
{
	std::list<const Schema::Object*> schemaStack;
	Schematise(*record.pObject, record.revision, schema, schemaStack);
	record.revision = m_revision;
}

void Database::Schematise(JObject& object, int revision, const Schema::Object& schema, std::list<const Schema::Object*>& schemaStack)
{
	schemaStack.push_back(&schema);

	// Renaming
	for (auto& rename : schema.m_rename)
	{
		JBase* jBase = object.Get(rename.oldName.c_str());

		if (jBase && revision < rename.revision)
		{
			JBase* pTemp(jBase->DeepCopy());
			object.Erase(rename.oldName);
			object.Insert(rename.newName, pTemp);
		}
	}

	// Remove any members of object not in the schema.
	for (auto it = object.GetMembers().begin(); it != object.GetMembers().end();)
	{
		auto field = schema.fields.find(it->first);

		if (field == schema.fields.end())
		{
			auto it2 = it; it++;
			object.Erase(it2->first);
			continue;
		}

		if (it->second->GetObject() && (field->second.isArray || field->second.basicType != Json::Schema::Type::Object))
		{
			auto it2 = it; it++;
			object.Erase(it2->first);
			continue;
		}

		if (it->second->GetArray() && !field->second.isArray)
		{
			auto it2 = it; it++;
			object.Erase(it2->first);
			continue;
		}

		if (it->second->GetString() && (field->second.isArray || field->second.basicType == Json::Schema::Type::Object))
		{
			auto it2 = it; it++;
			object.Erase(it2->first);
			continue;
		}

		it++;
	}

	// Add any members that are not present.
	for (auto& pair : schema.fields)
	{
		if (pair.second.isArray)
		{
			if (!object.Get(pair.first.c_str()))
			{
				object.Insert(pair.first, new JArray);
			}
			else
			{
				Json::JArray* jArray = object.GetArray(pair.first.c_str());

				for (int i = 0; i != jArray->Size(); ++i)
				{
					if (pair.second.basicType == Schema::Type::Object)
					{
						Schema::Object* pSchema = GetSchema(pair.second.typeName);

						if (std::find(schemaStack.begin(), schemaStack.end(), pSchema) == schemaStack.end())
						{
							if (!jArray->GetObject(i))
							{
								JObject* newObject = new JObject;
								jArray->Set(i, newObject);
							}

							if (pSchema)
							{
								Schematise(*jArray->GetObject(i), revision, *pSchema, schemaStack);
							}
						}
					}
					else
					{
						if (!jArray->Get(i)->GetString())
						{
							JData* newObject = new JData;
							jArray->Set(i, newObject);
						}

						Schematise((JData*)jArray->Get(i), revision, pair.second.basicType, pair.second.typeName);
					}
				}
			}
		}
		else if (pair.second.basicType == Json::Schema::Type::Object)
		{
			Schema::Object* pSchema = GetSchema(pair.second.typeName);

			if (std::find(schemaStack.begin(), schemaStack.end(), pSchema) == schemaStack.end())
			{
				if (!object.Get(pair.first.c_str()))
				{
					JObject* newObject = new JObject;
					object.Insert(pair.first, newObject);
				}

				if (pSchema)
				{
					Schematise(*object.GetObject(pair.first.c_str()), revision, *pSchema, schemaStack);
				}
			}
		}
		else
		{
			Json::JData* jData = (Json::JData*) object.Get(pair.first.c_str());

			if (!jData)
			{
				jData = new JData();
				object.Insert(pair.first, jData);
			}

			Schematise(jData, revision, pair.second.basicType, pair.second.typeName);
		}
	}

	schemaStack.pop_back();
}

std::string TypeName(const std::string name)
{
	std::string newString(name);

	newString.erase(std::remove(newString.begin(), newString.end(), ' '), newString.end());

	if (newString.size() > 0)
		newString[0] = toupper(newString[0]);

	return newString;
}

std::string VariableName(const std::string name)
{
	std::string newString(name);

	newString.erase(std::remove(newString.begin(), newString.end(), ' '), newString.end());

	if (newString.size() > 0)
		newString[0] = tolower(newString[0]);

	newString = "m_" + newString;

	return newString;
}

void WriteMembers(std::ofstream& file, const Json::Schema::Object& object)
{
	for (auto& pair : object.fields)
	{
		if (pair.second.basicType != Schema::Type::Null)
		{
			file << "\t";

			if (pair.second.isArray)
				file << "std::vector<";

			switch (pair.second.basicType)
			{
				case Schema::Type::Boolean: file << "bool"; break;
				case Schema::Type::Int: file << "int"; break;
				case Schema::Type::Float: file << "float"; break;
				case Schema::Type::String: file << "std::string"; break;
				case Schema::Type::Object: file << TypeName(pair.second.typeName); break;
				case Schema::Type::Enum: file << TypeName(pair.second.typeName); break;
				case Schema::Type::Reference: file << "Json::Reference<" + TypeName(pair.second.typeName) + ">"; break;
			}

			if (pair.second.isArray)
				file << ">";

			file << " " << VariableName(pair.first) << ";\n";
		}
	}
}

void WriteMembersForSerialisation(std::ofstream& file, const Json::Schema::Object& object)
{
	for (auto& pair : object.fields)
	{
		if (pair.second.basicType != Schema::Type::Null)
		{
			file << "\tar.Serialise(\"" + TypeName(pair.first) + "\", t."+ VariableName(pair.first) + ");\n";
		}
	}
}

void WriteMembersForFindReferences(std::ofstream& file, const Json::Schema::Object& object)
{
	for (auto& pair : object.fields)
	{
		if (pair.second.basicType == Schema::Type::Reference)
		{
			file << "\t";
			file << "t." + VariableName(pair.first) + ".SetReference(database.Get<" + TypeName(pair.second.typeName) + ">(" + "t." + VariableName(pair.first) + ".GetLocation()));";
			file << "\n";
		}
	}
}

bool Database::WriteEnum(std::ofstream& file, const std::string& name, const Schema::Enum& enum_) const
{
	file << "enum class " + TypeName(name) << "\n";
	file << "{" << "\n";

	file << "\t" << "Null" << ",\n";

	for (auto& s : enum_.values)
		file << "\t" << TypeName(s) << ",\n";

	file << "};" << "\n";
	file << "\n";

	file << "template <typename Archive>\n";
	file << "void Serialise(Archive& ar, " + TypeName(name) + "& t)\n";
	file << "{\n";

	file << "\tSERIALISATION_ENUM_BEGIN(" << TypeName(name) << ")\n";

	file << "\tSERIALISATION_ENUM_VALUE(" << TypeName(name) << "::" << "Null" << ", " << "Null" << ")\n";

	for (auto& s : enum_.values)
		file << "\tSERIALISATION_ENUM_VALUE(" << TypeName(name) << "::" << TypeName(s) << ", " << TypeName(s) << ")\n";

	file << "\tSERIALISATION_ENUM_END(" << TypeName(name) << ", t)\n";

	file << "}\n";
	file << "\n";

	return true;
}

bool Database::WriteDeclaration(std::ofstream& file, const std::string& name) const
{
	file << "struct " + TypeName(name) << ";\n";
	return true;
}

bool Database::WriteSchema(std::ofstream& file, const std::string& name, const Schema::Object& object) const
{
	if (m_started.find(name) != m_started.end())
		return false;

	m_started.insert(name);

	for (auto& pair : object.fields)
	{
		if (pair.second.basicType == Json::Schema::Type::Object)
		{
			if (m_written.find(pair.second.typeName) == m_written.end())
			{
				auto it = m_schemas.find(pair.second.typeName);

				if (it != m_schemas.end())
				{
					if (!WriteSchema(file, pair.second.typeName, it->second))
						return false;
				}
			}
		}
	}

	file << "struct " + TypeName(name) << "\n";
	file << "{" << "\n";
	Json::WriteMembers(file, object);
	file << "\n";
	file << "\tSERIALISATION_DECL_GUID(" << TypeName(m_name) + "::" + TypeName(name) <<")\n";
	file << "};" << "\n";
	file << "\n";

	file << "template <typename Archive>\n";
	file << "void Serialise(Archive& ar, " + TypeName(name) + "& t)\n";
	file << "{\n";
	Json::WriteMembersForSerialisation(file, object);
	file << "}\n";
	file << "\n";

	file << "inline void FindReferences(const Json::Database& database, " + TypeName(name) + "& t)\n";
	file << "{\n";
	Json::WriteMembersForFindReferences(file, object);
	file << "}\n";
	file << "\n";

	m_written.insert(name);

	return true;
}

bool Database::WriteRegisterTypes(std::ofstream& file) const
{
	file << "inline void RegisterTypes(Json::Database& database)" << "\n";
	file << "{" << "\n";
	file << "\tstatic bool done = false;\n";
	file << "\tif (!done)\n";
	file << "\t{\n";

	for (auto& pair : m_schemas)
		file << "\t\tSERIALISATION_REGISTER(Json::Serialiser, " << TypeName(pair.first) << ")" << "\n";

	file << "\t\tdone = true;\n";
	file << "\t}\n";
	file << "\n";

	for (auto& pair : m_schemas)
		file << "\tDATABASE_REGISTER(database, " << TypeName(pair.first) << ")" << "\n";

	file << "}\n";
	file << "\n";

	return true;
}

bool Database::WriteHeader(const char* filename) const
{
	std::ofstream file;
	file.open(filename);

	if (file.is_open())
	{
		bool success = true;

		file << "#pragma once" << "\n";
		file << "\n";
		file << "#include \"serialise/serialise.h\"\n";
		file << "#include \"serialise/serialisetypes.h\"\n";
		file << "#include \"serialise/reference.h\"\n";
		file << "#include \"serialise/database.h\"\n";
		file << "#include \"serialise/jsonserialise.h\"\n";
		file << "\n";

		{
			file << "namespace " + TypeName(m_name) << "\n";
			file << "{" << "\n";
			file << "\n";

			for (auto& pair : m_schemas)
			{
				if (!WriteDeclaration(file, pair.first))
					success = false;
			}

			file << "\n";

			for (auto& pair : m_enums)
			{
				if (!WriteEnum(file, pair.first, pair.second))
					success = false;
			}

			file << "}\n\n";
		}

		{
			file << "namespace Serialisation\n";
			file << "{\n";

			for (auto& pair : m_enums)
				file << "\tSERIALISATION_IS_ENUM(" << TypeName(m_name) << "::" << TypeName(pair.first) << ")\n";

			file << "}\n\n";
		}

		{
			file << "namespace " + TypeName(m_name) << "\n";
			file << "{" << "\n";
			file << "\n";

			for (auto& pair : m_schemas)
			{
				if (!WriteSchema(file, pair.first, pair.second))
					success = false;
			}

			WriteRegisterTypes(file);

			file << "}\n";
		}

		m_written.clear();
		m_started.clear();

		return success;
	}

	return false;
}

Record* Database::FindRecord(const char* location)
{
	std::string s(location);

	std::string::size_type pos = s.find("/");

	if (pos != std::string::npos)
		return GetRoot().FindRecord(FolderName(s.substr(pos + 1, s.size() - pos)));
			
	return 0;
}

Folder* Database::FindFolder(const char* location)
{
	std::string s(location);

	if (FolderName(s) == "Root")
		return &GetRoot();

	std::string::size_type pos = s.find("/");

	if (pos != std::string::npos)
		return GetRoot().FindFolder(FolderName(s.substr(pos + 1, s.size() - pos)));
			
	return 0;
}

const Record* Database::FindRecord(const char* location) const
{
	return const_cast<Database*>(this)->FindRecord(location);
}

const Folder* Database::FindFolder(const char* location) const
{
	return const_cast<Database*>(this)->FindFolder(location);
}

void Database::CreateObject(const std::string& path, const std::string& name, const Record& record, bool findReferences)
{
	Serialisation::Handler<Json::Serialiser>* pHandler = Serialisation::HandlerMap<Json::Serialiser>::GetHandler((TypeName(m_name) + "::" + TypeName(record.schema)).c_str());

	if (!findReferences)
	{
		if (pHandler)
		{
			void* vp;
			pHandler->DeserialiseDirect(Json::Deserialiser(record.pObject), vp);
			Ut::ReferenceCountedPointer* p = new Ut::ReferenceCountedPointer(vp, pHandler->GetDestructor());

			auto it = m_objects.emplace(path + name, Ut::SharedPtr<NullObject>()).first;

			it->second.SetReference(p);
		}
	}
	else
	{
		auto fn = m_findReferenceMap.find(TypeName(m_name) + "::" +TypeName(record.schema));

		if (fn != m_findReferenceMap.end())
			(*fn->second)(*this, *m_objects[path + name]);
	}
}

void Database::CreateObjects(const std::string& path, const Folder& folder, bool findReferences)
{
	for (auto& record : folder.m_records)
		CreateObject(path, record.first, record.second, findReferences);

	for (auto& folder : folder.m_folders)
		CreateObjects(path + folder.first + "/", folder.second, findReferences);
}

void Database::CreateObjects()
{
	CreateObjects("Root/", GetRoot(), false);
	CreateObjects("Root/", GetRoot(), true);
}

void Database::Register(const char* guid, FindReferencesFn* p)
{
	m_findReferenceMap[guid] = p;
}

int Database::IncrementRevision()
{
	return ++m_revision;
}

}